package ferp.android.managers;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import ferp.android.GUI;
import ferp.android.R;
import ferp.core.card.Card;
import ferp.core.card.Deck;
import ferp.core.game.Settings;
import ferp.core.log.Log;

import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * User: olegs
 * Date: 12/17/13 6:56 PM
 */
public class DeckManager
{
  public interface RefreshableView
  {
    void refresh();
  }

  public static DeckManager instance(Context context)
  {
    if (instance == null)
      instance = new DeckManager(context);

    return instance;
  }

  // path for external decks
  //public String path() {return Environment.getExternalStorageDirectory().getPath() + File.separator + DECKS;}
  public File folder()
  {
    File efd = context.getExternalFilesDir(null);

    return efd == null ? null : new File(efd, DECKS);
  }

  // register registered view
  public void register(RefreshableView sv) {views.add(sv);}
  // clear all registered views
  public void clear() {views.clear();}
  // get card image
  public Drawable getCardBack() {return images[0];}
  // get card image
  public Drawable getCardFront(Card c) {return images[c.compressed];}

  // get suit image for regular purposes
  public Drawable getSuitImage(Card.Suit suit, boolean enabled) {return images[Image.index(suit, enabled)];}

  // get suit image for tutorial note - clone the original image as tutorial note modifies its bounds
  public Drawable getTutorialSuitImage(Card.Suit suit)
  {
    int index = Image.index(suit);

    if (suits[index] == null)
      suits[index] = getSuitImage(suit, true).getConstantState().newDrawable();

    return suits[index];
  }

  // check whether load is required for specific deck
  public boolean isLoadRequired(Context context, String deck) {return this.deck == null || (!deck.equals(this.deck) && error(context, deck) == null);}

  // get last error occurred while loading deck
  public String error(Context context, String deck)
  {
    Status s = this.status.get(deck);

    return s == null ? context.getString(R.string.settings_deck_error_file_not_found, deck) : s.error;
  }

  // refresh images of all registered views
  public void refresh()
  {
    for (RefreshableView rv : views)
      rv.refresh();
  }

  public String[] decks()
  {
    File dir = folder();

    if (dir == null || !dir.exists())
      return STANDARD;

    File[] files = dir.listFiles(new FilenameFilter()
    {
      @Override
      public boolean accept(File dir, String name)
      {
        return name.endsWith(Extension.ZIP);
      }
    });

    if (files == null || files.length == 0)
      return STANDARD;

    invalidate(files);

    String[] decks = status.keySet().toArray(new String[files.length + 1]);
    Arrays.sort(decks, NameComparator.instance);

    return decks;
  }

  public void load(Context context, String deck)
  {
    if (deck.equals(Settings.DECK_STANDARD))
      cache(context);
    else
      cache(context, deck);
  }

  public boolean isReady() {return ready;}
  public void setReady(boolean ready) {this.ready = ready;}

  private final Context context;
  private boolean ready;

  private DeckManager(Context context)
  {
    this.context = context.getApplicationContext();

    status.put(Settings.DECK_STANDARD, Status.STANDARD);
    resolve(context, 0, Image.CARD_BACK);

    for (Card.Suit s : Card.Suit.values())
    {
      resolve(context, Image.index(s, true), Image.getSuitResourceName(s, true));
      resolve(context, Image.index(s, false), Image.getSuitResourceName(s, false));

      if (s != Card.Suit.NONE)
        for (Card.Rank r : Card.Rank.rising)
        {
          Card c = Card.card(s, r);

          resolve(context, c.compressed, Image.getCardResourceName(c));
        }
    }

    // read all deck files for the first time to initialize status map
    decks();
  }

  // resolve resource identifier and file name
  private void resolve(Context context, int index, String rn)
  {
    Resources r = context.getResources();

    identifiers[index] = r.getIdentifier(rn, "drawable", context.getPackageName());
    names[index] = Image.getFileName(rn);
  }

  private void cache(Context context, String deck)
  {
    Resources r = context.getResources();
    int success = 0;

    try
    {
      ZipFile zf = new ZipFile(new File(folder(), deck));

      for (int i = 0; i < names.length; ++i)
      {
        ZipEntry ze = zf.getEntry(names[i]);

        if (images[i] != null)
          GUI.recycle(images[i]);

        if (ze == null)
          images[i] = r.getDrawable(identifiers[i]);
        else
          try
          {
            images[i] = new BitmapDrawable(BitmapFactory.decodeStream(zf.getInputStream(ze)));
            success++;
          }
          catch (Exception e)
          {
            images[i] = r.getDrawable(identifiers[i]);
          }
      }

      // save deck name and reset error for it
      commit(deck);

      Log.debug(Log.TAG, "External deck '" + deck + "' cached (" + success + " images)");
    }
    catch (Exception e)
    {
      handleException(context, deck, e);
    }
  }

  private void cache(Context context)
  {
    Resources r = context.getResources();

    for (int i = 0; i < identifiers.length; ++i)
    {
      if (images[i] != null)
        GUI.recycle(images[i]);

      images[i] = r.getDrawable(identifiers[i]);
    }

    // save deck name and reset error for it
    commit(Settings.DECK_STANDARD);

    Log.debug(Log.TAG, "Standard deck cached");
  }

  private void handleException(Context context, String deck, Exception e)
  {
    Status s = status.get(deck);

    if (s != null)
      s.error = context.getResources().getString(R.string.settings_deck_error_load, deck);

    if (this.deck == null)
    {
      Log.debug(Log.TAG, "Error occurred while caching external deck '" + deck + "' (" + e.getMessage() + "). Fallback to standard images.");
      cache(context);
    }
    else
      Log.debug(Log.TAG, "Error occurred while caching external deck '" + deck + "' (" + e.getMessage() + "). Using last cached images from deck '" + this.deck + "'.");
  }

  private void invalidate(File[] decks)
  {
    HashMap<String, Status> fresh = new HashMap<String, Status>(decks.length + 1);

    // always add 'Standard' deck's status
    fresh.put(Settings.DECK_STANDARD, Status.STANDARD);

    for (File f : decks)
    {
      Status s = this.status.get(f.getName());

      if (s == null)
      {
        s = new Status(f.lastModified());

        Log.debug(Log.TAG, "New deck " + f.getName() + " has been determined");
      }
      else
        if (s.lastModified != f.lastModified())
        {
          s.lastModified = f.lastModified();
          s.error = null;

          Log.debug(Log.TAG, "Existing deck " + f.getName() + " has been changed");
        }

      fresh.put(f.getName(), s);
    }

    status = fresh;
  }

  private void commit(String deck)
  {
    Status s = status.get(deck);

    if (s != null)
      s.error = null;

    this.deck = deck;

    // every time a deck is cached, tutorial suits images must be invalidated to be reloaded on demand at next access
    Arrays.fill(suits, null);
  }

  private static final class Extension
  {
    private static final String ZIP = ".zip";
    private static final String PNG = ".png";
  }

  private static final class Image
  {
    private static final String CARD = "card_";
    private static final String SUIT = "suit_";
    private static final String CARD_BACK = CARD + "back";

    private static String getCardResourceName(Card c) {return CARD + c.suit.shortName() + c.rank.toString().toLowerCase();}
    private static String getSuitResourceName(Card.Suit s, boolean enabled) {return SUIT + s.shortName() + (enabled ? "_e" : "_d");}
    private static String getFileName(String rn) {return rn + Extension.PNG;}
    private static int index(Card.Suit suit, boolean enabled) {return Deck.SIZE + 1 + 2 * suit.ordinal() + (enabled ? 0 : 1);}
    private static int index(Card.Suit suit) {return suit.ordinal();}
  }

  private static final class Status
  {
    private static final Status STANDARD = new Status(0);

    private String error;
    private long lastModified;

    private Status(long lastModified) {this.lastModified = lastModified;}
  }

  private static final class NameComparator implements Comparator<String>
  {
    @Override
    public int compare(String s1, String s2)
    {
      if (s1.equals(Settings.DECK_STANDARD) && !s2.equals(Settings.DECK_STANDARD))
        return -1;

      if (!s1.equals(Settings.DECK_STANDARD) && s2.equals(Settings.DECK_STANDARD))
        return 1;

      return s1.compareTo(s2);
    }

    @Override
    public boolean equals(Object o) {return false;}

    private static final NameComparator instance = new NameComparator();
  }

  // cache size
  private static final int SIZE = Deck.SIZE + 1 + 2 * Card.Suit.values().length;
  // decks folder
  private static final String DECKS = "decks";
  // constant array that contains 'Standard' deck only
  private static final String[] STANDARD = {Settings.DECK_STANDARD};

  private static DeckManager instance = null;

  // identifiers of standard images
  private final int[] identifiers = new int[SIZE];
  // required file names
  private final String[] names = new String[SIZE];
  // cache of regular images
  private Drawable[] images = new Drawable[SIZE];
  // cache of suit images for tutorial (on demand)
  private Drawable[] suits = new Drawable[Card.Suit.values().length];
  // registered views
  private LinkedList<RefreshableView> views = new LinkedList<RefreshableView>();
  // status hash map
  private HashMap<String, Status> status = new HashMap<String, Status>();
  // current deck's name
  private String deck;
}
